// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

export default function (CodeMirror) {
  var defaults = {
    pairs: '()[]{}\'\'""',
    closeBefore: ')]}\'":;>',
    triples: '',
    explode: '[]{}',
  };

  var Pos = CodeMirror.Pos;

  CodeMirror.defineOption('autoCloseBrackets', false, function (cm, val, old) {
    if (old && old != CodeMirror.Init) {
      cm.removeKeyMap(keyMap);
      cm.state.closeBrackets = null;
    }
    if (val) {
      ensureBound(getOption(val, 'pairs'));
      cm.state.closeBrackets = val;
      cm.addKeyMap(keyMap);
    }
  });

  function getOption(conf, name) {
    if (name == 'pairs' && typeof conf == 'string') return conf;
    if (typeof conf == 'object' && conf[name] != null) return conf[name];
    return defaults[name];
  }

  var keyMap = { Backspace: handleBackspace, Enter: handleEnter };
  function ensureBound(chars) {
    for (var i = 0; i < chars.length; i++) {
      var ch = chars.charAt(i),
        key = "'" + ch + "'";
      if (!keyMap[key]) keyMap[key] = handler(ch);
    }
  }
  ensureBound(defaults.pairs + '`');

  function handler(ch) {
    return function (cm) {
      return handleChar(cm, ch);
    };
  }

  function getConfig(cm) {
    var deflt = cm.state.closeBrackets;
    if (!deflt || deflt.override) return deflt;
    var mode = cm.getModeAt(cm.getCursor());
    return mode.closeBrackets || deflt;
  }

  function handleBackspace(cm) {
    var conf = getConfig(cm);
    if (!conf || cm.getOption('disableInput')) return CodeMirror.Pass;

    var pairs = getOption(conf, 'pairs');
    var ranges = cm.listSelections();
    for (var i = 0; i < ranges.length; i++) {
      if (!ranges[i].empty()) return CodeMirror.Pass;
      var around = charsAround(cm, ranges[i].head);
      if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
    }
    for (var i = ranges.length - 1; i >= 0; i--) {
      var cur = ranges[i].head;
      cm.replaceRange('', Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), '+delete');
    }
  }

  function handleEnter(cm) {
    var conf = getConfig(cm);
    var explode = conf && getOption(conf, 'explode');
    if (!explode || cm.getOption('disableInput')) return CodeMirror.Pass;

    var ranges = cm.listSelections();
    for (var i = 0; i < ranges.length; i++) {
      if (!ranges[i].empty()) return CodeMirror.Pass;
      var around = charsAround(cm, ranges[i].head);
      if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
    }
    cm.operation(function () {
      var linesep = cm.lineSeparator() || '\n';
      cm.replaceSelection(linesep + linesep, null);
      moveSel(cm, -1);
      ranges = cm.listSelections();
      for (var i = 0; i < ranges.length; i++) {
        var line = ranges[i].head.line;
        cm.indentLine(line, null, true);
        cm.indentLine(line + 1, null, true);
      }
    });
  }

  function moveSel(cm, dir) {
    var newRanges = [],
      ranges = cm.listSelections(),
      primary = 0;
    for (var i = 0; i < ranges.length; i++) {
      var range = ranges[i];
      if (range.head == cm.getCursor()) primary = i;
      var pos =
        range.head.ch || dir > 0 ? { line: range.head.line, ch: range.head.ch + dir } : { line: range.head.line - 1 };
      newRanges.push({ anchor: pos, head: pos });
    }
    cm.setSelections(newRanges, primary);
  }

  function contractSelection(sel) {
    var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
    return {
      anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
      head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1)),
    };
  }

  function handleChar(cm, ch) {
    var conf = getConfig(cm);
    if (!conf || cm.getOption('disableInput')) return CodeMirror.Pass;

    var pairs = getOption(conf, 'pairs');
    var pos = pairs.indexOf(ch);
    if (pos == -1) return CodeMirror.Pass;

    var closeBefore = getOption(conf, 'closeBefore');

    var triples = getOption(conf, 'triples');

    var identical = pairs.charAt(pos + 1) == ch;
    var ranges = cm.listSelections();
    var opening = pos % 2 == 0;

    var type;
    for (var i = 0; i < ranges.length; i++) {
      var range = ranges[i],
        cur = range.head,
        curType;
      var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
      if (opening && !range.empty()) {
        curType = 'surround';
      } else if ((identical || !opening) && next == ch) {
        if (identical && stringStartsAfter(cm, cur)) curType = 'both';
        else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
          curType = 'skipThree';
        else curType = 'skip';
      } else if (
        identical &&
        cur.ch > 1 &&
        triples.indexOf(ch) >= 0 &&
        cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch
      ) {
        if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
        curType = 'addFour';
      } else if (identical) {
        var prev = cur.ch == 0 ? ' ' : cm.getRange(Pos(cur.line, cur.ch - 1), cur);
        if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = 'both';
        else return CodeMirror.Pass;
      } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
        curType = 'both';
      } else {
        return CodeMirror.Pass;
      }
      if (!type) type = curType;
      else if (type != curType) return CodeMirror.Pass;
    }

    var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
    var right = pos % 2 ? ch : pairs.charAt(pos + 1);
    cm.operation(function () {
      if (type == 'skip') {
        moveSel(cm, 1);
      } else if (type == 'skipThree') {
        moveSel(cm, 3);
      } else if (type == 'surround') {
        var sels = cm.getSelections();
        for (var i = 0; i < sels.length; i++) sels[i] = left + sels[i] + right;
        cm.replaceSelections(sels, 'around');
        sels = cm.listSelections().slice();
        for (var i = 0; i < sels.length; i++) sels[i] = contractSelection(sels[i]);
        cm.setSelections(sels);
      } else if (type == 'both') {
        cm.replaceSelection(left + right, null);
        cm.triggerElectric(left + right);
        moveSel(cm, -1);
      } else if (type == 'addFour') {
        cm.replaceSelection(left + left + left + left, 'before');
        moveSel(cm, 1);
      }
    });
  }

  function charsAround(cm, pos) {
    var str = cm.getRange(Pos(pos.line, pos.ch - 1), Pos(pos.line, pos.ch + 1));
    return str.length == 2 ? str : null;
  }

  function stringStartsAfter(cm, pos) {
    var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1));
    return (
      /\bstring/.test(token.type) && token.start == pos.ch && (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
    );
  }
}
